// 再研读。。。
#include <bits/stdc++.h>
using namespace std;

using ll = long long;
using pii = array<int, 2>; // 存储最大值和出现次数的pair

const int MAXN = 300010; // 最大N值
const int INF = 1e18;    // 无穷大值

int n, q; // 集合数量n和查询数量q

// 线段树全局数组
int lazy_tag[MAXN * 4 + 9]; // 懒标记数组
pii tree[MAXN * 4 + 9];     // 线段树节点数组，存储{最大值, 出现次数}

set<array<int, 2>> interval_sets[62]; // 存储每个值x（1-60）对应的区间集合

// 节点合并操作：当两个子区间的最大值相等时，次数相加；否则取较大的值
pii merge_nodes(const pii &a, const pii &b) {
  if (a[0] == b[0]) {
    return {a[0], a[1] + b[1]};
  }
  return max(a, b); // 取最大值
}

// 上推操作：将子节点的信息合并到父节点
void push_up(int node) {
  tree[node] = merge_nodes(tree[node << 1], tree[node << 1 | 1]);
}

// 下推懒标记：将当前节点的懒标记传递给子节点
void push_down(int node) {
  if (lazy_tag[node]) {
    // 更新左子节点
    tree[node << 1][0] += lazy_tag[node];
    tree[node << 1 | 1][0] += lazy_tag[node];
    // 更新左子节点的懒标记
    lazy_tag[node << 1] += lazy_tag[node];
    lazy_tag[node << 1 | 1] += lazy_tag[node];

    
    lazy_tag[node] = 0; // 清空当前节点的懒标记
  }
}

// 构建线段树：初始化每个叶子节点的值为0，出现次数为1
void build(int node, int left, int right) {
  if (left == right) {
    // 叶子节点：初始值为0，出现次数为1
    tree[node] = {0, 1};
  } else {
    int mid = (left + right) >> 1;
    build(node << 1, left, mid);          // 构建左子树
    build(node << 1 | 1, mid + 1, right); // 构建右子树
    push_up(node);                        // 合并子节点信息
  }
}

// 区间查询：查询区间[query_left, query_right]的最大值信息
pii query(int node, int left, int right, int query_left,
                       int query_right) {
  // 查询区间与当前区间无交集
  if (query_left > right || query_right < left) {
    return {-INF, 0}; // 返回无效值
  }
  // 当前区间完全包含在查询区间内
  if (query_left <= left && query_right >= right) {
    return tree[node];
  }
  push_down(node); // 下推懒标记
  int mid = (left + right) >> 1;
  pii result = {-INF, 0}; // 初始化结果
  // 查询左子树
  if (mid >= query_left) {
    result = query(node << 1, left, mid, query_left, query_right);
  }
  // 查询右子树并合并结果
  if (mid < query_right) {
    pii right_result = query(node << 1 | 1, mid + 1, right,
                                          query_left, query_right);
    result = merge_nodes(result, right_result);
  }
  return result;
}

// 区间更新：在区间[update_left, update_right]上增加value
void update(int node, int left, int right, int update_left,
                         int update_right, int value) {
  // 更新区间与当前区间无交集
  if (update_left > right || update_right < left)
    return;
  // 当前区间完全包含在更新区间内
  if (update_left <= left && update_right >= right) {
    tree[node][0] += value;  // 更新节点值
    lazy_tag[node] += value; // 设置懒标记
    return;
  }
  push_down(node); // 下推懒标记
  int mid = (left + right) >> 1;
  // 更新左子树
  if (mid >= update_left) {
    update(node << 1, left, mid, update_left, update_right, value);
  }
  // 更新右子树
  if (mid < update_right) {
    update(node << 1 | 1, mid + 1, right, update_left,
                        update_right, value);
  }
  push_up(node); // 上推更新后的信息
}

/**
 * 在指定值的区间集合中分割区间
 * 在position位置分割包含该位置的区间，确保position成为区间边界
 */
void split_interval(int value, int position) {
  // 找到第一个起点大于position的区间
  auto it = interval_sets[value].lower_bound({position + 1, 0});
  if (it == interval_sets[value].begin())
    return; // 没有合适的区间
  it--;     // 移动到包含position的区间

  auto [left, right] = *it;
  if (right < position)
    return; // 该区间不包含position

  // 删除原区间
  interval_sets[value].erase({left, right});
  // 如果position左边有区间，插入左半部分
  if (left <= position - 1) {
    interval_sets[value].insert({left, position - 1});
  }
  // 如果position右边有区间，插入右半部分
  if (position <= right) {
    interval_sets[value].insert({position, right});
  }
}

/**
 * 清除指定值在区间[left, right]内的所有区间
 * 并在线段树中减去这些区间的贡献
 */
void clear_intervals(int value, int left, int right) {
  while (true) {
    // 找到第一个起点大于等于left的区间
    auto it = interval_sets[value].lower_bound({left, -INF});
    // 如果没有区间或区间起点大于right，结束循环
    if (it == interval_sets[value].end() || (*it)[0] > right)
      return;

    auto [interval_left, interval_right] = *it;
    interval_sets[value].erase({interval_left, interval_right});
    // 在线段树中减去该区间的贡献（每个位置减1）
    update(1, 1, n, interval_left, interval_right, -1);
  }
}

/**
 * 主处理函数
 */
void solve() {
  cin >> n >> q;  // 输入集合数量和查询数量
  build(1, 1, n); // 构建线段树

  // 初始化每个值的区间集合，添加边界哨兵防止越界
  for (int i = 1; i <= 60; i++) {
    interval_sets[i].insert({-INF, -INF}); // 左边界哨兵
    interval_sets[i].insert({INF, INF});   // 右边界哨兵
  }

  // 处理每个查询
  for (int i = 1; i <= q; i++) {
    int operation, left, right;
    cin >> operation >> left >> right;

    if (operation == 1) { // 类型1：添加元素x到区间内的所有集合
      int value;
      cin >> value;

      // 步骤1：在left和right+1位置分割区间，确保边界准确
      split_interval(value, left);
      split_interval(value, right + 1);

      // 步骤2：清除[left, right]内原有的包含value的区间
      clear_intervals(value, left, right);

      // 步骤3：在线段树中为[left, right]内的每个集合增加1个元素
      update(1, 1, n, left, right, 1);

      // 步骤4：将新区间[left, right]添加到value的区间集合中
      interval_sets[value].insert({left, right});

    } else if (operation == 2) { // 类型2：从区间内的所有集合中删除元素x
      int value;
      cin >> value;

      // 步骤1：在left和right+1位置分割区间
      split_interval(value, left);
      split_interval(value, right + 1);

      // 步骤2：清除[left, right]内所有包含value的区间
      clear_intervals(value, left, right);
      // 注意：这里不添加新区间，只是删除操作

    } else { // 类型3：查询区间内集合元素数量的最大值及其出现次数
      auto [max_value, count] = query(1, 1, n, left, right);
      cout << max_value << ' ' << count << '\n';
    }
  }
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0);

  solve();

  return 0;
}